#pragma once

#include <condition_variable>
#include <mutex>
#include <type_traits>

// A header-only, C++17-compatible writer-preferred shared mutex.
//
// Goals:
//  - Block new readers when there is at least one waiting writer.
//  - Wake a writer first when the last reader leaves.
//  - API mirrors a subset of std::shared_mutex: lock/try_lock/unlock,
//    lock_shared/try_lock_shared/unlock_shared.
//  - Portable: default implementation uses std::mutex + std::condition_variable.
//
// Notes:
//  - This class is non-recursive. Acquiring the same mode twice from the same
//    thread is undefined behavior (deadlock) — same as std::shared_mutex.
//  - No timed locking API is provided to keep the implementation concise.
//  - Strong exception safety: all operations are noexcept.
//
// Usage:
//    shared_mutex_wp m;
//    // readers:
//    m.lock_shared();
//    ...
//    m.unlock_shared();
//    // writer:
//    m.lock();
//    ...
//    m.unlock();
//
//    // With RAII helpers (std::shared_lock / std::unique_lock):
//    std::shared_lock<shared_mutex_wp> rlk(m);
//    std::unique_lock<shared_mutex_wp> wlk(m);
//
// Thread-safety model:
//  - Multiple concurrent readers allowed when no writer holds/awaits.
//  - If any writer is waiting, subsequent readers will wait (writer-preferred).
//

class shared_mutex_wp {
public:
    shared_mutex_wp() noexcept = default;
    ~shared_mutex_wp() noexcept = default;

    shared_mutex_wp(const shared_mutex_wp&) = delete;
    shared_mutex_wp& operator=(const shared_mutex_wp&) = delete;

    // Exclusive (writer) locking
    void lock() noexcept
    {
        std::unique_lock<std::mutex> lk(m_);
        ++writers_waiting_;
        // Wait until no writer holds the lock and no readers present
        // (writer-preference is enforced by blocking new readers while writers_waiting_ > 0).
        cv_writers_.wait(lk, [this] { return !writer_active_ && readers_ == 0; });
        --writers_waiting_;
        writer_active_ = true;
    }

    bool try_lock() noexcept
    {
        std::unique_lock<std::mutex> lk(m_, std::try_to_lock);
        if (!lk.owns_lock()) {
            return false;
        }
        if (writer_active_ || readers_ != 0) {
            return false;
        }
        writer_active_ = true;
        return true;
    }

    void unlock() noexcept
    {
        std::lock_guard<std::mutex> lk(m_);
        writer_active_ = false;
        // Prefer writers first. If none waiting, wake all readers.
        if (writers_waiting_ > 0) {
            cv_writers_.notify_one();
        } else {
            cv_readers_.notify_all();
        }
    }

    // Shared (reader) locking
    void lock_shared() noexcept
    {
        std::unique_lock<std::mutex> lk(m_);
        // Writer preference: block new readers if a writer is waiting or active.
        cv_readers_.wait(lk, [this] { return !writer_active_ && writers_waiting_ == 0; });
        ++readers_;
    }

    bool try_lock_shared() noexcept
    {
        std::unique_lock<std::mutex> lk(m_, std::try_to_lock);
        if (!lk.owns_lock()) {
            return false;
        }
        if (writer_active_ || writers_waiting_ > 0) {
            return false;
        }
        ++readers_;
        return true;
    }

    void unlock_shared() noexcept
    {
        std::lock_guard<std::mutex> lk(m_);
        // Last reader wakes one writer (if any).
        if (--readers_ == 0 && writers_waiting_ > 0) {
            cv_writers_.notify_one();
        }
    }

    // For compatibility with Lockable concepts in the standard library
    using native_handle_type = void*;
    native_handle_type native_handle() noexcept
    {
        return nullptr;
    }

private:
    // State guarded by m_
    std::mutex m_;
    std::condition_variable cv_readers_;
    std::condition_variable cv_writers_;
    unsigned readers_ = 0;         // number of active readers
    unsigned writers_waiting_ = 0; // number of writers waiting
    bool writer_active_ = false;   // a writer currently holds the lock
};

// Convenience aliases so std::unique_lock / std::shared_lock work naturally.
static_assert(std::is_nothrow_default_constructible<shared_mutex_wp>::value, "must be nothrow default constructible");
static_assert(noexcept(std::declval<shared_mutex_wp&>().lock()), "lock must be noexcept");
static_assert(noexcept(std::declval<shared_mutex_wp&>().lock_shared()), "lock_shared must be noexcept");
